<?php

	/**
	 * Path
	 *
	 * Performs operations on string instances that contain file or directory path information.
	 * These operations are performed in a cross-platform manner
	 *
	 * @package      Core
	 * @subpackage   IO
	 *
     * @license      GNU Lesser General Public Licence see LICENCE-LGPL file or http://www.gnu.org/licenses/lgpl.html
	 */
	class Core_IO_Path
	{

	////////////////////////////////////////////////////////////////////////////
	//                            Getters / Setters                           //
	////////////////////////////////////////////////////////////////////////////

		/**
		 * Provides a platform-specific array of characters that cannot be specified in path string arguments passed to members of the Path class
		 *
		 * @return		array
		 */
		public static function getInvalidPathChars()
		{
			return array('"', '<', '>', '|', '\0', chr(1), chr(2), chr(3), chr(4), chr(5), chr(6), chr(7), chr(8), chr(9), chr(10), chr(11), chr(12), chr(13), chr(14), chr(15), chr(16), chr(17), chr(18), chr(19), chr(20), chr(21), chr(22), chr(23), chr(24), chr(25), chr(26), chr(27), chr(28), chr(29), chr(30), chr(31));
		}

		/**
		 * Provides a platform-specific volume separator character
		 *
		 * @return		char
		 */
		public static function getVolumeSeparatorChar()
		{
			return (!Core_Helper_Environment::isWin32()) ? '/' : ':';
		}

	////////////////////////////////////////////////////////////////////////////
	//                             Static Methods                             //
	////////////////////////////////////////////////////////////////////////////

		/**
		 * Changes the extension of a path string
		 *
		 * @param		string					$path : The path information to modify. The path cannot contain any of the characters defined by getInvalidPathChars
		 * @param		string					$extension : The new extension (with a leading period). Specify a null reference to remove an existing extension from path
		 * @return		string					A string containing the modified path information. On Windows-based desktop platforms, if path is a null reference or an empty string (""), the path information is returned unmodified. If extension is a null reference, the returned string contains the specified path with its extension removed. If path has no extension, and extension is not a null reference, the returned path string contains extension appended to the end of path
		 */
		public static function changeExtension($path, $extension)
		{
            if ($path != null) {
                self::checkInvalidPathChars($path);

                $s = $path;
				$length = strlen($path);
                for ($i = $length; --$i >= 0;) {
                    $ch = $path[$i];
                    if ($ch == '.') {
                        $s = substr($path, 0, $i);
                        break;
                    }

                    if ($ch == DIRECTORY_SEPARATOR || $ch == self::getInvalidPathChars()) {
						break;
					}
                }

                if ($extension != null && strlen($path) != 0)
				{
                    if (strlen(trim($extension)) == 0 || $extension[0] != '.') {
						$s .= '.';
					}

                    $s .= $extension;
                }

                return $s;
            }

            return null;
		}

		/**
		 * Returns the directory information for the specified path string
		 *
		 * @param		string					$path : The path of a file or directory
		 * @return		string					A string containing directory information for path, or a null reference if path denotes a root directory, is the empty string (""), or is a null reference .
		 * 										Returns '' if path does not contain directory information
		 */
        public static function getDirectoryName($path)
		{
			if ($path != null) {
				self::checkInvalidPathChars($path);

                $root = self::_getRootLength($path);
                $i = strlen($path);

                if ($i > $root) {
                    $i = strlen($path);
                    if ($i == $root) {
						return null;
					}

                    while ($i > $root && $path[--$i] != DIRECTORY_SEPARATOR);
                    return substr($path, 0, $i);
                }
            }

            return null;
        }

		/**
		 * Combines two path strings
		 *
		 * @param		array					$args : undefined length array
		 * @throws		Core_ArgumentException
		 * @return		string					A string containing the combined paths. If one of the specified paths is a zero-length string, this method returns the other path. If path2 contains an absolute path, this method returns path2
		 */
		public static function combine()
		{
			$args = func_get_args();
			if (empty($args) || count($args) < 2) {
                throw new Core_ArgumentException("Combine must have at least two arguments");
            }

			for ($i = 0; $i < count($args); $i++) {
                self::checkInvalidPathChars($args[$i]);
    			$args[$i] = rtrim($args[$i], DIRECTORY_SEPARATOR);
            }

			return implode(DIRECTORY_SEPARATOR, $args);
		}

		/**
		 * Returns the extension of the specified path string
		 *
		 * @param		string					$path : The path string from which to get the extension
		 * @return		string					A string containing the extension of the specified path (including the "."), a null reference, or Empty. If path is a null reference, GetExtension returns a null reference. If path does not have extension information, getExtension returns Empty
		 */
        public static function getExtension($path)
		{
            if ($path === null) {
				return null;
            }

            self::checkInvalidPathChars($path);

			$length = strlen($path);
            for ($i = $length; --$i >= 0;) {
                $ch = $path[$i];
                if ($ch == '.') {
                    if ($i != $length - 1) {
						return substr($path, $i, $length - $i);
					} else {
						return '';
					}
                }

                if ($ch == DIRECTORY_SEPARATOR || $ch == self::getVolumeSeparatorChar()) {
                    break;
                }
            }

            return '';
        }

		/**
		 * Returns the file name and extension of the specified path string
		 *
		 * @param		string					$path : The path string from which to obtain the file name and extension
		 * @return		string					A string consisting of the characters after the last directory character in path. If the last character of path is a directory or volume separator character, this method returns Empty. If path is a null reference, this method returns a null reference.
		 */
        public static function getFileName($path)
		{
          if ($path != null) {
                self::checkInvalidPathChars($path);

				$length = strlen($path);
                for ($i = $length; --$i >= 0;) {
                    $ch = $path[$i];
                    if ($ch == DIRECTORY_SEPARATOR || $ch == self::getVolumeSeparatorChar()) {
                        return substr($path, $i + 1, $length - $i - 1);
                    }
                }
            }

			return $path;
        }

		/**
		 * Returns the file name of the specified path string without the extension
		 *
		 * @param		string					$path : The path of the file
		 * @return		string					A string containing the string returned by GetFileName, minus the last period (.) and all characters following it
		 */
		public static function getFileNameWithoutExtension($path)
		{
			$path = self::getFileName($path);

			if ($path != null) {
				if (($i = strrpos($path, '.')) === false) {
					return $path; // No path extension found
				} else {
					return substr($path, 0, $i);
				}
			}

			return null;
		}

		/**
		 * Determines whether a path includes a file name extension
		 *
		 * @param		string					$path : The path to search for an extension
		 * @return		boolean					true if the characters that follow the last directory separator (\\ or /) or volume separator (:) in the path include a period (.) followed by one or more characters; otherwise, false
		 */
		public static function hasExtension($path)
		{
			if ($path != null) {
				self::checkInvalidPathChars($path);

				$length = strlen($path);
				for ($i = $length; --$i >= 0;) {
					$ch = $path[$i];
					if ($ch == '.') {
						if ($i != $length - 1) {
							return true;
						} else {
							return false;
						}
					}

					if ($ch == DIRECTORY_SEPARATOR || $ch == self::getVolumeSeparatorChar()) {
						break;
					}
				}
			}

			return false;
		}

		/**
		 * Gets the root directory information of the specified path
		 *
		 * @param		string					$path : The path from which to obtain root directory information
		 * @return		string					A string containing the root directory of path, such as "C:\", or a null reference if path is a null reference, or an empty string if path does not contain root directory information
		 */
		public static function getPathRoot($path)
		{
            if ($path === null) {
				return null;
			}

            return substr($path, 0, self::_getRootLength($path));
        }

		/**
		 * Gets a value indicating whether the specified path string contains absolute or relative path information
		 *
		 * @param		string					$path : The path to test
		 * @return		boolean					true if path contains an absolute path, otherwise, false
		 */
        public static function isPathRooted($path)
		{
            if ($path != null) {
                self::checkInvalidPathChars($path);

				$length = strlen($path);
				if (Core_Helper_Environment::isWin32()) {
	                if (($length >= 1 && $path[0] == DIRECTORY_SEPARATOR) || ($length >= 2 && $path[1] == self::getVolumeSeparatorChar()))
					{
						return true;
					}
				} else if ($length >= 1 && ($path[0] == DIRECTORY_SEPARATOR)) {
					return true;
				}
            }

            return false;
        }

		/**
		 * Checks that the path doesn't contain invalid characters
		 *
		 * @param		string					$path : The path to test
		 * @throws		Core_ArgumentException
		 * @return		void
		 */
        public static function checkInvalidPathChars($path)
		{
			if (!Core_Helper_Environment::isWin32()) {
	            if (substr($path, 0, 1) == "\\\\") {
	                throw new Core_ArgumentException("path contains one or more of the invalid characters defined in InvalidPathChars, or contains a wildcard character");
	            }
			}

            $length = strlen($path);
			for ($i = 0; $i < $length; $i++) {
                $c = $path[$i];

                // Note: This list is duplicated in static char[] InvalidPathChars
                if ($c == '"' || $c == '<' || $c == '>' || $c == '|' || $c < chr(32)) {
                    throw new Core_ArgumentException("path contains one or more of the invalid characters defined in InvalidPathChars, or contains a wildcard character");
                }
            }
        }

	////////////////////////////////////////////////////////////////////////////
	//                            Internal Methods                            //
	////////////////////////////////////////////////////////////////////////////

        /**
		 * Indicates wether a character is a directory separator
		 *
		 * @param		char		$c
		 * @return		boolean
		 */
		protected static function _isDirectorySeparator($c)
		{
            return ($c == DIRECTORY_SEPARATOR) ? true : false;
        }

        protected static function _getRootLength($path)
		{
            self::checkInvalidPathChars($path);

            $i = 0;
			$length = strlen($path);
			if (!Core_Helper_Environment::isWin32()) {
	            if ($length >= 1 && (self::_isDirectorySeparator($path[0]))) {
					// handles UNC names and directories off current drive's root.
	                $i = 1;
	                if ($length >= 2 && self::_isDirectorySeparator($path[1])) {
	                    $i = 2;
	                    $n = 2;

	                    while ($i < $length && (($path[$i] != DIRECTORY_SEPARATOR) || --$n > 0)) {
							$i++;
						}
	                }
	            } else if ($length >= 2 && $path[1] == self::getVolumeSeparatorChar()) {
	                // handles A:\foo.
	                $i = 2;
	                if ($length >= 3 && (self::_isDirectorySeparator($path[2]))) {
						$i++;
					}
	            }
			} else if ($length >= 1 && (self::_isDirectorySeparator($path[0]))) {
				$i = 1;
			}

			return $i;
        }
	}
